/*************************************************************************************************
  Required Notice: Copyright (C) EPPlus Software AB. 
  This software is licensed under PolyForm Noncommercial License 1.0.0 
  and may only be used for noncommercial purposes 
  https://polyformproject.org/licenses/noncommercial/1.0.0/

  A commercial license to use this software can be purchased at https://epplussoftware.com
 *************************************************************************************************
  Date               Author                       Change
 *************************************************************************************************
  01/27/2020         EPPlus Software AB       Initial release EPPlus 5
 *************************************************************************************************/
using OfficeOpenXml.Encryption;
using OfficeOpenXml.Utils.CompundDocument;
using System;
using System.IO;
#if !NET35 && !NET40
using System.Threading.Tasks;
#endif
namespace OfficeOpenXml
{
    public sealed partial class ExcelPackage
    {
#if !NET35 && !NET40
        #region Load
        /// <summary>
        /// Loads the specified package data from a stream.
        /// </summary>
        /// <param name="fileInfo">The input file.</param>
        public async Task LoadAsync(FileInfo fileInfo)
        {
            var stream = fileInfo.OpenRead();
            await LoadAsync(stream, new MemoryStream(), null).ConfigureAwait(false);
        }
        /// <summary>
        /// Loads the specified package data from a stream.
        /// </summary>
        /// <param name="fileInfo">The input file.</param>
        /// <param name="Password">The password</param>
        public async Task LoadAsync(FileInfo fileInfo, string Password)
        {
            var stream = fileInfo.OpenRead();
            await LoadAsync(stream, new MemoryStream(), Password).ConfigureAwait(false);
        }
        /// <summary>
        /// Loads the specified package data from a stream.
        /// </summary>
        /// <param name="fileInfo">The input file.</param>
        /// <param name="output">The out stream. Sets the Stream property</param>
        /// <param name="Password">The password</param>
        public async Task LoadAsync(FileInfo fileInfo, Stream output, string Password)
        {
            var stream = fileInfo.OpenRead();
            await LoadAsync(stream, output, Password).ConfigureAwait(false);
        }
        /// <summary>
        /// Loads the specified package data from a stream.
        /// </summary>
        /// <param name="input">The input.</param>
        public async Task LoadAsync(Stream input)
        {
            await LoadAsync(input, new MemoryStream(), null).ConfigureAwait(false);
        }
        /// <summary>
        /// Loads the specified package data from a stream.
        /// </summary>
        /// <param name="input">The input.</param>
        /// <param name="Password">The password to decrypt the document</param>
        public async Task LoadAsync(Stream input, string Password)
        {
            await LoadAsync(input, new MemoryStream(), Password).ConfigureAwait(false); ;
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="input"></param>    
        /// <param name="output"></param>
        /// <param name="Password"></param>
        private async Task LoadAsync(Stream input, Stream output, string Password)
        {
            ReleaseResources();
            if (input.Length == 0) // Template is blank, Construct new
            {
                _stream = output;
                await ConstructNewFileAsync(Password).ConfigureAwait(false);
            }
            else
            {
                Stream ms;
                _stream = output;
                if (Password != null)
                {
                    Stream encrStream = new MemoryStream();
                    await CopyStreamAsync(input, encrStream).ConfigureAwait(false);
                    EncryptedPackageHandler eph = new EncryptedPackageHandler();
                    Encryption.Password = Password;
                    ms = eph.DecryptPackage((MemoryStream)encrStream, Encryption);
                }
                else
                {
                    ms = new MemoryStream();
                    await CopyStreamAsync(input, ms).ConfigureAwait(false);
                }

                try
                {
                    _package = new Packaging.ZipPackage(ms);
                }
                catch (Exception ex)
                {
                    EncryptedPackageHandler eph = new EncryptedPackageHandler();
                    if (Password == null && await CompoundDocumentFile.IsCompoundDocumentAsync((MemoryStream)_stream).ConfigureAwait(false))
                    {
                        throw new Exception("Can not open the package. Package is an OLE compound document. If this is an encrypted package, please supply the password", ex);
                    }
                    else
                    {
                        throw;
                    }
                }
            }
            //Clear the workbook so that it gets reinitialized next time
            this._workbook = null;
        }
        #endregion
        #region SaveAsync
        /// <summary>
        /// Saves all the components back into the package.
        /// This method recursively calls the Save method on all sub-components.
        /// The package is closed after it has ben saved
        /// d to encrypt the workbook with. 
        /// </summary>
        /// <returns></returns>
        public async Task SaveAsync()
        {
            try
            {
                if (_stream is MemoryStream && _stream.Length > 0)
                {
                    //Close any open memorystream and "renew" then. This can occure if the package is saved twice. 
                    //The stream is left open on save to enable the user to read the stream-property.
                    //Non-memorystream streams will leave the closing to the user before saving a second time.
                    CloseStream();
                }

                //Invoke before save delegates
                foreach (var action in BeforeSave)
                {
                    action.Invoke();
                }

                Workbook.Save();
                if (File == null)
                {
                    if (Encryption.IsEncrypted)
                    {
                        var ms = new MemoryStream();
                        _package.Save(ms);
                        byte[] file = ms.ToArray();
                        EncryptedPackageHandler eph = new EncryptedPackageHandler();
                        var msEnc = eph.EncryptPackage(file, Encryption);
                        await CopyStreamAsync(msEnc, _stream).ConfigureAwait(false);
                    }
                    else
                    {
                        _package.Save(_stream);
                    }
                    _stream.Flush();
                    _package.Close();
                }
                else
                {
                    if (System.IO.File.Exists(File.FullName))
                    {
                        try
                        {
                            System.IO.File.Delete(File.FullName);
                        }
                        catch (Exception ex)
                        {
                            throw (new Exception(string.Format("Error overwriting file {0}", File.FullName), ex));
                        }
                    }

                    _package.Save(_stream);
                    _package.Close();
                    if (Stream is MemoryStream)
                    {
                        var fi = new FileStream(File.FullName, FileMode.Create);
                        //EncryptPackage
                        if (Encryption.IsEncrypted)
                        {
                            byte[] file = ((MemoryStream)Stream).ToArray();
                            EncryptedPackageHandler eph = new EncryptedPackageHandler();
                            var ms = eph.EncryptPackage(file, Encryption);

                            await fi.WriteAsync(ms.ToArray(), 0, (int)ms.Length).ConfigureAwait(false);
                        }
                        else
                        {
                            await fi.WriteAsync(((MemoryStream)Stream).ToArray(), 0, (int)Stream.Length).ConfigureAwait(false);
                        }
                        fi.Close();
                        fi.Dispose();
                    }
                    else
                    {
                        using (FileStream fs = new FileStream(File.FullName, FileMode.Create))
                        {
                            var b = await GetAsByteArrayAsync(false).ConfigureAwait(false);
                            await fs.WriteAsync(b, 0, b.Length).ConfigureAwait(false);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                if (File == null)
                {
                    throw;
                }
                else
                {
                    throw (new InvalidOperationException(string.Format("Error saving file {0}", File.FullName), ex));
                }
            }
        }
        /// <summary>
        /// Saves all the components back into the package.
        /// This method recursively calls the Save method on all sub-components.
        /// The package is closed after it has ben saved
        /// Supply a password to encrypt the workbook package. 
        /// </summary>
        /// <param name="password">This parameter overrides the Workbook.Encryption.Password.</param>
        public async Task SaveAsync(string password)
        {
            Encryption.Password = password;
            await SaveAsync().ConfigureAwait(false);
        }
        #endregion
        #region SaveAsAsync
        /// <summary>
        /// Saves the workbook to a new file
        /// The package is closed after it has been saved        
        /// </summary>
        /// <param name="file">The file location</param>
        public async Task SaveAsAsync(FileInfo file)
        {
            File = file;
            await SaveAsync().ConfigureAwait(false); 
        }
        /// <summary>
        /// Saves the workbook to a new file
        /// The package is closed after it has been saved
        /// </summary>
        /// <param name="file">The file</param>
        /// <param name="password">The password to encrypt the workbook with. 
        /// This parameter overrides the Encryption.Password.</param>
        public async Task SaveAsAsync(FileInfo file, string password)
        {
            File = file;
            Encryption.Password = password;
            await SaveAsync().ConfigureAwait(false);
        }
        /// <summary>
        /// Copies the Package to the Outstream
        /// The package is closed after it has been saved
        /// </summary>
        /// <param name="OutputStream">The stream to copy the package to</param>
        public async Task SaveAsAsync(Stream OutputStream)
        {
            File = null;
            await SaveAsync().ConfigureAwait(false); 

            if (OutputStream != _stream)
            {
                await CopyStreamAsync(_stream, OutputStream).ConfigureAwait(false);
            }
        }
        /// <summary>
        /// Copies the Package to the Outstream
        /// The package is closed after it has been saved
        /// </summary>
        /// <param name="OutputStream">The stream to copy the package to</param>
        /// <param name="password">The password to encrypt the workbook with. 
        /// This parameter overrides the Encryption.Password.</param>
        public async Task SaveAsAsync(Stream OutputStream, string password)
        {
            Encryption.Password = password;
            await SaveAsAsync(OutputStream).ConfigureAwait(false); 
        }
        #endregion
        /// <summary>
        /// Copies the input stream to the output stream.
        /// </summary>
        /// <param name="inputStream">The input stream.</param>
        /// <param name="outputStream">The output stream.</param>
        internal static async Task CopyStreamAsync(Stream inputStream, Stream outputStream)
        {
            if (!inputStream.CanRead)
            {
                throw (new Exception("Can not read from inputstream"));
            }
            if (!outputStream.CanWrite)
            {
                throw (new Exception("Can not write to outputstream"));
            }
            if (inputStream.CanSeek)
            {
                inputStream.Seek(0, SeekOrigin.Begin);
            }

            const int bufferLength = 8096;
            var buffer = new Byte[bufferLength];
            int bytesRead = await inputStream.ReadAsync(buffer, 0, bufferLength).ConfigureAwait(false);
            // write the required bytes
            while (bytesRead > 0)
            {
                await outputStream.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false);
                bytesRead = await inputStream.ReadAsync(buffer, 0, bufferLength).ConfigureAwait(false);
            }
            await outputStream.FlushAsync();
        }
        internal async Task<byte[]> GetAsByteArrayAsync(bool save)
        {
            if (save)
            {
                Workbook.Save();
                _package.Close();
                _package.Save(_stream);
            }
            Byte[] byRet = new byte[Stream.Length];
            long pos = Stream.Position;
            Stream.Seek(0, SeekOrigin.Begin);
            await Stream.ReadAsync(byRet, 0, (int)Stream.Length).ConfigureAwait(false);

            //Encrypt Workbook?
            if (Encryption.IsEncrypted)
            {
                EncryptedPackageHandler eph = new EncryptedPackageHandler();
                var ms = eph.EncryptPackage(byRet, Encryption);
                byRet = ms.ToArray();
            }

            Stream.Seek(pos, SeekOrigin.Begin);
            Stream.Close();
            return byRet;
        }
        /// <summary>
        /// Saves and returns the Excel files as a bytearray.
        /// Note that the package is closed upon save
        /// </summary>
        /// <example>      
        /// Example how to return a document from a Webserver...
        /// <code> 
        ///  ExcelPackage package=new ExcelPackage();
        ///  /**** ... Create the document ****/
        ///  Byte[] bin = package.GetAsByteArray();
        ///  Response.ContentType = "Application/vnd.ms-Excel";
        ///  Response.AddHeader("content-disposition", "attachment;  filename=TheFile.xlsx");
        ///  Response.BinaryWrite(bin);
        /// </code>
        /// </example>
        /// <returns></returns>
        public async Task<byte[]> GetAsByteArrayAsync()
        {
            return await GetAsByteArrayAsync(true).ConfigureAwait(false);
        }
        /// <summary>
        /// Saves and returns the Excel files as a bytearray
        /// Note that the package is closed upon save
        /// </summary>
        /// <example>      
        /// Example how to return a document from a Webserver...
        /// <code> 
        ///  ExcelPackage package=new ExcelPackage();
        ///  /**** ... Create the document ****/
        ///  Byte[] bin = package.GetAsByteArray();
        ///  Response.ContentType = "Application/vnd.ms-Excel";
        ///  Response.AddHeader("content-disposition", "attachment;  filename=TheFile.xlsx");
        ///  Response.BinaryWrite(bin);
        /// </code>
        /// </example>
        /// <param name="password">The password to encrypt the workbook with. 
        /// This parameter overrides the Encryption.Password.</param>
        /// <returns></returns>
        public async Task<byte[]> GetAsByteArrayAsync(string password)
        {
            if (password != null)
            {
                Encryption.Password = password;
            }
            return await GetAsByteArrayAsync(true).ConfigureAwait(false);
        }
        private async Task ConstructNewFileAsync(string password)
        {
            var ms = new MemoryStream();
            if (_stream == null) _stream = new MemoryStream();
            if (File != null) File.Refresh();
            if (File != null && File.Exists)
            {
                if (password != null)
                {
                    var encrHandler = new EncryptedPackageHandler();
                    Encryption.IsEncrypted = true;
                    Encryption.Password = password;
                    ms = encrHandler.DecryptPackage(File, Encryption);
                    encrHandler = null;
                }
                else
                {
                    await WriteFileToStreamAsync(File.FullName, ms);
                }
                try
                {
                    _package = new Packaging.ZipPackage(ms);
                }
                catch (Exception ex)
                {
                    if (password == null && await CompoundDocumentFile.IsCompoundDocumentAsync(File).ConfigureAwait(false))
                    {
                        throw new Exception("Can not open the package. Package is an OLE compound document. If this is an encrypted package, please supply the password", ex);
                    }
                    else
                    {
                        throw;
                    }
                }
            }
            else
            {
                _package = new Packaging.ZipPackage(ms);
                CreateBlankWb();
            }
        }
        private static async Task WriteFileToStreamAsync(string path, Stream stream)
        {
            using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            {
                var buffer = new byte[4096];
                int read;
                while ((read = await fileStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0)
                {
                    await stream.WriteAsync(buffer, 0, read).ConfigureAwait(false);
                }
            }
        }

#endif
    }
}
